package com.third.utils.wx;

import java.io.BufferedReader;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.third.base.wx.WXPayConstants;
import com.third.base.wx.WXPayConstants.SignType;
import com.third.base.wx.WXPayRequest;
import com.third.base.wx.WXPaySignUtil;



	/**
	* @ClassName: 微信支付：刷卡支付、公众号支付、扫码支付、app支付、h5支付、小程序支付
	* @Description:微信支付功能：代金券或立减优惠、现金红包、企业付款
	* @author likai
	* @date 2017年10月14日
	* @微信支付文档：https://pay.weixin.qq.com/wiki/doc/api/index.html
	*/
public class WxPayUtil{
	public static Logger log = LoggerFactory.getLogger(WxPayUtil.class);

    private WxPayConfig config;//微信支付配置
    private SignType signType;//签名类型MD5, HMACSHA256
    private boolean autoReport;
    private boolean useSandbox;//是否是沙箱环境true是
    private String notifyUrl;//通知回掉路径
    private WXPayRequest wxPayRequest;//支付请求体数据
    private static WxPayUtil wxPay;

    private WxPayUtil(final WxPayConfig config) {
        this(config, config.getNotifyUrl(), true, config.getUseSandbox());
    }

   

    private WxPayUtil(final WxPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox){
        this.config = config;
        this.notifyUrl = notifyUrl;
        this.autoReport = autoReport;
        this.useSandbox = useSandbox;
        if (useSandbox) {
            this.signType = SignType.MD5; // 沙箱环境
        }
        else {
            this.signType = SignType.MD5;
        }
        this.wxPayRequest = new WXPayRequest(config);
    }
    public static WxPayUtil create(WxPayConfig config){
		if (wxPay==null) {
			wxPay=new WxPayUtil(config);
		}
		return wxPay;
	}

    /*----------------------------------------------------微信支付：刷卡支付、公众号支付、扫码支付、app支付、h5支付、小程序支付------------------------------------------------------------*/
    /*--------------------------------------具体参数和返回结果请查看微信官方文档---------------------------------------------------------------------------*/

    /**
     * 作用：刷卡支付,失败不重试<br>
     * 场景：刷卡支付
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> microPay(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_MICROPAY_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.MICROPAY_URL_SUFFIX;
        }
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData));
        return this.processResponseXml(respXml);
    }



    /**
     * 刷卡支付，针对软POS，失败重试
     * 内置重试机制，最多60s
     * @param reqData
     * @return
     * @throws Exception
     */
    public Map<String, String> microPayWithPos(Map<String, String> reqData) throws Exception {
        int remainingTimeMs = 60*1000;
        long startTimestampMs = 0;
        Map<String, String> lastResult = null;
        Exception lastException = null;

        while (true) {
            startTimestampMs = WXPaySignUtil.getCurrentTimestampMs();
            int readTimeoutMs = remainingTimeMs - this.config.getHttpConnectTimeoutMs();
            if (readTimeoutMs > 1000) {
                try {
                    lastResult = this.microPay(reqData);
                    String returnCode = lastResult.get("return_code");
                    if (returnCode.equals("SUCCESS")) {
                        String resultCode = lastResult.get("result_code");
                        String errCode = lastResult.get("err_code");
                        if (resultCode.equals("SUCCESS")) {
                            break;
                        }
                        else {
                            // 看错误码，若支付结果未知，则重试提交刷卡支付
                            if (errCode.equals("SYSTEMERROR") || errCode.equals("BANKERROR") || errCode.equals("USERPAYING")) {
                                remainingTimeMs = remainingTimeMs - (int)(WXPaySignUtil.getCurrentTimestampMs() - startTimestampMs);
                                if (remainingTimeMs <= 100) {
                                    break;
                                }
                                else {
                                    WXPaySignUtil.getLogger().info("microPayWithPos: try micropay again");
                                    if (remainingTimeMs > 5*1000) {
                                        Thread.sleep(5*1000);
                                    }
                                    else {
                                        Thread.sleep(1*1000);
                                    }
                                    continue;
                                }
                            }
                            else {
                                break;
                            }
                        }
                    }
                    else {
                        break;
                    }
                }
                catch (Exception ex) {
                    lastResult = null;
                    lastException = ex;
                }
            }
            else {
                break;
            }
        }

        if (lastResult == null) {
            throw lastException;
        }
        else {
            return lastResult;
        }
    }


    /**
     * 作用：统一下单支付<br>
     * 场景：公共号支付、扫码支付、APP支付,h5支付,小程序支付
     * @param 必传参数：body、out_trade_no、total_fee、spbill_create_ip、notify_url、trade_type:WXPayConfig中配置
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> unifiedOrder(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_UNIFIEDORDER_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.UNIFIEDORDER_URL_SUFFIX;
        }
        if(this.notifyUrl != null) {
            reqData.put("notify_url", this.notifyUrl);
        }
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData));
        return this.processResponseXml(respXml);
    }




    
    /*-------------------------------------微信支付工具：代金券或立减优惠、现金红包、企业付款-------------------------------------------------*/
    /*-------------------------------------代金券或立减优惠-------------------------------------------------*/
	
	 /**
    * 作用：发放代金券<br>
    * 场景：发放代金券
    * @param reqData 向wxpay post的请求数据
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> sendCoupon(Map<String, String> reqData) throws Exception {      
	   String respXml = this.requestWithCert(WXPayConstants.SEND_COUPON, this.fillRequestData(reqData));
       return this.processResponseXml(respXml);
   }
    
   /**
    * 作用：查询代金券批次<br>
    * 场景：查询代金券批次
    * @param reqData 向wxpay post的请求数据
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> queryCouponStock(Map<String, String> reqData) throws Exception {      
	   String respXml = this.requestWithoutCert(WXPayConstants.QUERY_COUPON_STOCK, this.fillRequestData(reqData));
       return this.processResponseXml(respXml);
   }
    
    
   /**
    * 作用：查询代金券信息<br>
    * 场景：查询代金券信息
    * @param reqData 向wxpay post的请求数据
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> queryCouponsInfo(Map<String, String> reqData) throws Exception {      
	   String respXml = this.requestWithoutCert(WXPayConstants.QUERYCOUPONSINFO, this.fillRequestData(reqData));
       return this.processResponseXml(respXml);
   }
     
    
   /*-------------------------------------现金红包-------------------------------------------------*/
    
   /**
    * 作用：发放普通红包<br>
    * 场景：向用户发放普通红包
    * @param reqData 向wxpay post的请求数据
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> sendRedPack(Map<String, String> reqData) throws Exception {      
	   String respXml = this.requestWithCert(WXPayConstants.SENDREDPACK, this.fillRequestData(reqData));
       return this.processResponseXml(respXml);
   }
   
   
   /**
    * 作用：发放裂变红包<br>
    * 场景：向用户发放裂变红包
    * 说明：裂变红包：一次可以发放一组红包。首先领取的用户为种子用户，种子用户领取一组红包当中的一个，并可以通过社交分享将剩下的红包给其他用户。裂变红包充分利用了人际传播的优势。 
    * @param reqData 向wxpay post的请求数据
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> sendGroupRedPack(Map<String, String> reqData) throws Exception {      
	   String respXml = this.requestWithCert(WXPayConstants.SENDGROUPREDPACK, this.fillRequestData(reqData));
	   return this.processResponseXml(respXml);
   }
   
   
   /**
    * 作用：查询红包记录<br>
    * 场景：查询红包记录
    * @param reqData 向wxpay post的请求数据
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> gethbinfo(Map<String, String> reqData) throws Exception {      
	   String respXml = this.requestWithCert(WXPayConstants.GETHBINFO, this.fillRequestData(reqData));
	   return this.processResponseXml(respXml);
   }
   
   
   /*-------------------------------------企业付款-------------------------------------------------*/

   
   /**
    * 作用：企业付款<br>
    * 场景：提供企业向用户付款的功能，支持企业通过API接口付款，或通过微信支付商户平台网页功能操作付款。
    * @param reqData 向wxpay post的请求数据
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> transfers(Map<String, String> reqData) throws Exception {      
	   String respXml = this.requestWithCert(WXPayConstants.TRANSFERS,this.transeRequestData(reqData));
	   return WXPaySignUtil.xmlToMap(respXml);
   }
   
   /**
    * 作用：查询企业付款<br>
    * 场景：查询企业付款
    * @param reqData 向wxpay post的请求数据
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> getTransferInfo(Map<String, String> reqData) throws Exception {      
	   String respXml = this.requestWithCert(WXPayConstants.GETTRANSFERINFO, this.fillRequestData(reqData));
	   return this.processResponseXml(respXml);
   }
     
    
    
   
   
   
   
   /*-------------------------------------支付结果处理查询接口-------------------------------------------------*/

   /**
    * 作用：查询订单<br>
    * 场景：刷卡支付、公共号支付、扫码支付、APP支付
    * @param reqData 向wxpay post的请求数据 int
    * @return API返回数据
    * @throws Exception
    */
   public Map<String, String> orderQuery(Map<String, String> reqData) throws Exception {
       String url;
       if (this.useSandbox) {
           url = WXPayConstants.SANDBOX_ORDERQUERY_URL_SUFFIX;
       }
       else {
           url = WXPayConstants.ORDERQUERY_URL_SUFFIX;
       }
       String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData));
       return this.processResponseXml(respXml);
   }


    /**
     * 作用：撤销订单<br>
     * 场景：刷卡支付<br>
     * 其他：需要证书
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> reverse(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_REVERSE_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.REVERSE_URL_SUFFIX;
        }
        String respXml = this.requestWithCert(url, this.fillRequestData(reqData));
        return this.processResponseXml(respXml);
    }




    /**
     * 作用：关闭订单<br>
     * 场景：公共号支付、扫码支付、APP支付
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> closeOrder(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_CLOSEORDER_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.CLOSEORDER_URL_SUFFIX;
        }
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData));
        return this.processResponseXml(respXml);
    }



    /**
     * 作用：申请退款<br>
     * 场景：刷卡支付、公共号支付、扫码支付、APP支付<br>
     * 其他：需要证书
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> refund(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_REFUND_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.REFUND_URL_SUFFIX;
        }
        String respXml = this.requestWithCert(url, this.fillRequestData(reqData));
        return this.processResponseXml(respXml);
    }


    /**
     * 作用：退款查询<br>
     * 场景：刷卡支付、公共号支付、扫码支付、APP支付
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> refundQuery(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_REFUNDQUERY_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.REFUNDQUERY_URL_SUFFIX;
        }
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData));
        return this.processResponseXml(respXml);
    }



    /**
     * 作用：对账单下载（成功时返回对账单数据，失败时返回XML格式数据<br>
     * 场景：刷卡支付、公共号支付、扫码支付、APP支付<br>
     * 其他：无论是否成功都返回Map。若成功，返回的Map中含有return_code、return_msg、data，
     *      其中return_code为`SUCCESS`，data为对账单数据。
     * @param reqData 向wxpay post的请求数据
     * @return 经过封装的API返回数据
     * @throws Exception
     */
    public Map<String, String> downloadBill(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_DOWNLOADBILL_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.DOWNLOADBILL_URL_SUFFIX;
        }
        String respStr = this.requestWithoutCert(url, this.fillRequestData(reqData)).trim();
        Map<String, String> ret;
        // 出现错误，返回XML数据
        if (respStr.indexOf("<") == 0) {
            ret = WXPaySignUtil.xmlToMap(respStr);
        }
        else {
            // 正常返回csv数据
            ret = new HashMap<String, String>();
            ret.put("return_code", WXPayConstants.SUCCESS);
            ret.put("return_msg", "ok");
            ret.put("data", respStr);
        }
        return ret;
    }



    /**
     * 作用：交易保障<br>
     * 场景：刷卡支付、公共号支付、扫码支付、APP支付
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> report(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_REPORT_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.REPORT_URL_SUFFIX;
        }
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData));
        return WXPaySignUtil.xmlToMap(respXml);
    }




    /**
     * 作用：转换短链接<br>
     * 场景：刷卡支付、扫码支付
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> shortUrl(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_SHORTURL_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.SHORTURL_URL_SUFFIX;
        }
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData));
        return this.processResponseXml(respXml);
    }



    /**
     * 作用：授权码查询OPENID接口<br>
     * 场景：刷卡支付
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> authCodeToOpenid(Map<String, String> reqData) throws Exception {
        String url;
        if (this.useSandbox) {
            url = WXPayConstants.SANDBOX_AUTHCODETOOPENID_URL_SUFFIX;
        }
        else {
            url = WXPayConstants.AUTHCODETOOPENID_URL_SUFFIX;
        }
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData));
        return this.processResponseXml(respXml);
    }


    /**
     * 不需要证书的请求
     * @param urlSuffix 请求路径
     * @param reqData 向wxpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public String requestWithoutCert(String urlSuffix, Map<String, String> reqData) throws Exception {
        String msgUUID = reqData.get("nonce_str");
        String reqBody = WXPaySignUtil.mapToXml(reqData);
        String resp = this.wxPayRequest.requestWithoutCert(urlSuffix, msgUUID, reqBody,this.config.getHttpConnectTimeoutMs(),this.config.getHttpReadTimeoutMs(),this.autoReport);
        return resp;
    }


    /**
     * 需要证书的请求
     * @param urlSuffix String
     * @param reqData 向wxpay post的请求数据  Map
     * @return API返回数据
     * @throws Exception
     */
    public String requestWithCert(String urlSuffix, Map<String, String> reqData) throws Exception {
        String msgUUID= reqData.get("nonce_str");
        String reqBody = WXPaySignUtil.mapToXml(reqData);
        String resp = this.wxPayRequest.requestWithCert(urlSuffix, msgUUID, reqBody,this.config.getHttpConnectTimeoutMs(),this.config.getHttpReadTimeoutMs(), this.autoReport);
        return resp;
    }

  
    
    
    
    /*--------------------------------------------------------------工具方法------------------------------------------------------------------*/
    
    
    
    
    
    

    /**
     * 向请求参数 Map 中添加 appid、mch_id、nonce_str、sign_type、sign <br>
     * 该函数适用于商户适用于统一下单等接口，不适用于红包、代金券接口
     *
     * @param reqData
     * @return
     * @throws Exception
     */
    public Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
        reqData.put("appid", config.getAppID());
        reqData.put("mch_id", config.getMchID());
        reqData.put("nonce_str", WXPaySignUtil.generateUUID());
        if (SignType.MD5.equals(this.signType)) {
            reqData.put("sign_type", WXPayConstants.MD5);
        }
        else if (SignType.HMACSHA256.equals(this.signType)) {
            reqData.put("sign_type", WXPayConstants.HMACSHA256);
        }
        reqData.put("sign", WXPaySignUtil.generateSignature(reqData, config.getKey(), this.signType));
        return reqData;
    }
  
    /**
     * 向请求参数 Map 中添加 appid、mch_id、nonce_str、sign_type、sign <br>
     * 该函数适用于商户转账到零钱
     *
     * @param reqData
     * @return
     * @throws Exception
     */
    public Map<String, String> transeRequestData(Map<String, String> reqData) throws Exception {
        reqData.put("mch_appid", config.getAppID());//公众号或小程序号appid
        reqData.put("mchid", config.getMchID());//商户号
		reqData.put("check_name", "NO_CHECK");
        reqData.put("nonce_str", WXPaySignUtil.generateUUID());
        System.out.println(config.getMchID());
        reqData.put("sign", WXPaySignUtil.generateSignature(reqData, config.getKey(), SignType.MD5));
        return reqData;
    }
    /**
     * 处理微信创建支付订单中返回数据，转换成Map对象。result_code为SUCCESS时，验证签名。
     * @param xmlStr API返回的XML格式数据
     * @return Map类型数据
     * @throws Exception
     */
    public Map<String, String> processResponseXml(String xmlStr) throws Exception {
        String RETURN_CODE = "return_code";
        String return_code;
        Map<String, String> respData = WXPaySignUtil.xmlToMap(xmlStr);
        if (respData.containsKey(RETURN_CODE)) {
            return_code = respData.get(RETURN_CODE);
        }else {
            throw new Exception(String.format("返回结果不存在return_code: %s", xmlStr));
        }

        if (return_code.equals(WXPayConstants.FAIL)) {
        	throw new Exception(String.format("创建支付订单失败: %s", xmlStr));
        }else if (return_code.equals(WXPayConstants.SUCCESS)) {
        	if (WXPayConstants.SUCCESS.equals(respData.get("result_code"))) {				
        		if (this.isPayResultNotifySignatureValid(respData)) {
        			return respData;
        		}else {
        			throw new Exception(String.format("返回结果签名验证不匹配: %s", xmlStr));
        		}
			}else {
				throw new Exception(String.format("创建支付订单失败: %s", xmlStr));
			}
        }else {
            throw new Exception(String.format("return_code  %s 不可用: %s", return_code, xmlStr));
        }
    }
    
    /**
     * 处理 支付回调数据，验证签名，转换成Map对象
     * @param xmlStr API返回的XML格式数据
     * @return Map类型数据
     * @throws Exception
     */
    public Map<String, String> processNotify(HttpServletRequest request) throws Exception {
        String RETURN_CODE = "return_code";
        String return_code;
        String xmlStr=getNotifyXmlString(request);
        Map<String, String> respData = WXPaySignUtil.xmlToMap(xmlStr);
        if (respData.containsKey(RETURN_CODE)) {
            return_code = respData.get(RETURN_CODE);
        }else {
            throw new Exception(String.format("返回结果不存在return_code: %s", xmlStr));
        }
        if (return_code.equals(WXPayConstants.FAIL)) {
        	throw new Exception(String.format("通信标识微信支付失败return_code: %s", xmlStr));
        }else if (return_code.equals(WXPayConstants.SUCCESS)) {
        	if (WXPayConstants.SUCCESS.equals(respData.get("result_code"))) {				
        		if (this.isPayResultNotifySignatureValid(respData)) {
        			return respData;
        		}else {
        			throw new Exception(String.format("返回结果签名验证不匹配: %s", xmlStr));
        		}
			}else {
				throw new Exception(String.format("交易标识微信支付失败result_code: %s", xmlStr));
			}
        }else {
            throw new Exception(String.format("通信标识微信支付失败return_code  %s 不可用: %s", return_code, xmlStr));
        }
    }

    /**
     * 判断支付结果通知中的sign是否有效
     *
     * @param reqData 向wxpay post的请求数据
     * @return 签名是否有效
     * @throws Exception
     */
    public boolean isPayResultNotifySignatureValid(Map<String, String> reqData) throws Exception {
        String signTypeInData = reqData.get(WXPayConstants.FIELD_SIGN_TYPE);
        SignType signType;
        if (signTypeInData == null) {
            signType = SignType.MD5;
        }
        else {
            signTypeInData = signTypeInData.trim();
            if (signTypeInData.length() == 0) {
                signType = SignType.MD5;
            }
            else if (WXPayConstants.MD5.equals(signTypeInData)) {
                signType = SignType.MD5;
            }
            else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) {
                signType = SignType.HMACSHA256;
            }
            else {
                throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData));
            }
        }
        return WXPaySignUtil.isSignatureValid(reqData, this.config.getKey(), signType);
    }
    
    /**
     * 生成支付签名
     *
     * @param data 待签名数据
     * @return 签名
     */
    public String generateSignature(final Map<String, String> data) throws Exception {
        return WXPaySignUtil.generateSignature(data, config.getKey(),signType);
    }

    
    //-------------------------------------------静态工具方法-----------------------------------------
    
    /**
     * 解析微信支付回调的数据
     * 
     * @param request
     * @return
     * @throws Exception 
     */
    public static String getNotifyXmlString(HttpServletRequest request) throws Exception {
        BufferedReader reader = null;
        String line = "";
        String xmlString = null;
        try {
            reader = request.getReader();
            StringBuffer inputString = new StringBuffer();

            while ((line = reader.readLine()) != null) {
                inputString.append(line);
            }
            xmlString = inputString.toString();
        } catch (Exception e) {
            throw new Exception("回调数据解析失败");
        }

        return xmlString;
    }

    /**
     * 接收支付回调后返回给微信服务端的xml
     * @param return_code
     * @return
     */
    public static String returnNotifyXML(String return_code,String msg) {

        return "<xml><return_code><![CDATA["

                + return_code

                + "]]></return_code><return_msg><![CDATA["
                + msg
                + "]]></return_msg></xml>";
    }
    
    
} 
